iT邦幫忙

2026 iThome 鐵人賽

DAY 17
0
Software Development

打造 OS Kernel:從 OS in 1,000 Lines 到 xv6系列 第 17 篇

Day 17|System Call:User Program 第一次向 Kernel 求助

  • 分享至 

  • xImage
  •  

昨天核心已經把使用者程式放進記憶體,但還沒有執行它。
今天要讓終端機出現 U-mode says hello,而且這段文字必須由 U-mode 程式提出輸出請求,再由核心完成。

同樣是印字,這次多了一條權限邊界。
使用者程式不能直接呼叫核心內部的 putchar(),我們要替它設計一個受控入口。

函式呼叫不等於系統呼叫

《Operating System Concepts》第 10 版第 2.3 節介紹系統呼叫:應用程式透過約定的介面請求作業系統服務,核心依呼叫編號與參數選擇處理方式。
這個概念今天會落到三個地方:使用者程式的 ecall、Trap Frame 中的參數,以及核心的 dispatcher。

一般 C 函式呼叫只改變控制流程,不會自行把 U-mode 變成 S-mode。
RISC-V 的 ecall 則會觸發例外,CPU 依權限與委派設定進入對應的處理入口。
在本系列使用的 OpenSBI 環境中,U-mode 的 environment call 由我們的 S-mode 核心處理,scause 為 8。

這和核心向 OpenSBI 求助的 SBI 呼叫不同:前者是 U → S,後者是 S → M,不能只因為兩者都使用 ecall 就混為一談。

先約定最小的呼叫介面

今天不採用 Linux 的 syscall 編號,也不宣稱符合 POSIX。
我們只需要一份能由兩端共同遵守的約定:

暫存器/編號 意義
a7 系統呼叫編號
a0 第一個參數,返回時也是結果
編號 1 輸出 a0 的低 8 位元,成功回傳 0
編號 2 印出結束碼,停止這次單一使用者程式實驗
未知編號 回傳 0xffffffff,視為有號整數時是 −1

例如 user.S 中的輸出請求相當於:

li a0, 'H'
li a7, 1
ecall

程式會先呼叫不存在的編號 999,確認錯誤回傳值,再逐字印出文字。
這樣我們不只測成功路徑,也能確認 dispatcher 沒有把未知編號當成有效功能。

Checkpoint 1:從 S-mode 進入 U-mode

完整入口位於 user_entry.S。
呼叫端傳入使用者入口、使用者堆疊頂端,以及獨立核心堆疊的頂端:

csrw sepc, a0
csrw sscratch, a2
li t0, 0x40122
csrc sstatus, t0
mv sp, a1
sret

sepc 指向 Day 16 載入的程式,sret 的目的權限由 sstatus.SPP 決定。
這裡清除 SPP,讓返回目標成為 U-mode。

遮罩還清除了 SUM、SPIE 與 SIE。
本次尚未啟用非同步中斷,核心也不依靠 SUM 直接讀寫使用者映射,因此要把這些條件明確固定下來。
這不是可直接套用到完整搶占式核心的通用初始化範本。

Checkpoint 2:Trap 不能繼續用使用者的堆疊

進入 Trap 時,CPU 不會替我們自動換成核心堆疊,也不會保存全部通用暫存器。
如果立刻在使用者的 sp 上建立 Trap Frame,等於讓使用者決定核心要把重要狀態存在哪裡。

因此 trap.S 採用以下約定:

  • 執行 U-mode 時,sscratch 保存核心堆疊頂端
  • 執行核心 handler 時,sscratch 為 0
  • Trap entry 先交換 sp 與 sscratch,依交換結果區分入口情況

來自 U-mode 時,新的 sp 就是核心堆疊,原本的 user sp 則暫存在 sscratch。
入口接著保存所有需要恢復的通用暫存器,以及 sepc 與 sstatus,再呼叫 C handler。

返回前的順序也很重要:先準備下一次 Trap 所需的 sscratch,恢復其他暫存器,最後才恢復 user sp 並執行 sret。
這份範例沒有巢狀例外復原機制,核心內出現意外 Trap 會停止並列出診斷資訊。

Checkpoint 3:把請求轉成核心動作

閱讀 user.c 的 handle_trap(),最前面的檢查是:

unsigned int cause = READ_CSR(scause);
if (cause != 8 || (f->sstatus & 256)) {
    printf("trap cause=%u sepc=0x%x stval=0x%x\n",
           cause, f->sepc, READ_CSR(stval));
    PANIC("unexpected trap");
}
unsigned int id = f->x[17];
f->sepc += 4;

x[17] 對應 a7,x[10] 對應 a0。
核心修改 Trap Frame 中的 a0,返回組語就會把結果帶回使用者程式。

這裡能將 sepc 增加 4,是因為已確認目前處理的是固定 32-bit 長度的 ecall。
不能把同樣做法套在每一種例外,尤其這個 RV32IMAC 專案也允許 16-bit 壓縮指令。
若完全不調整 sepc,返回後又會執行同一個 ecall,看起來就像核心不斷收到相同請求。

把整條往返路徑跑一次

在 30-days-os-kernel/examples/tiny-kernel/ 執行:

make DAY=17 run

頁表檢查訊息之後,預期看到:

enter U-mode
U-mode says hello
user exit=0

這段輸出代表使用者程式通過未知編號測試,成功完成多次字元輸出,再帶著結束碼 0 回到核心。
編號 2 目前會讓整個實驗停在 halt(),不是完整的行程終止與排程交接。
結束 QEMU 可按 Ctrl+A 後按 X。

另外確認使用者映像真的含有系統呼叫指令:

llvm-objdump -d build/day17/user.elf

找到未知編號測試,以及輸出迴圈中的 ecall。
若輸出停在 enter U-mode,優先檢查 Trap 診斷、程式頁的 U/X 權限,以及 user sp 是否落在可寫頁面。

留下今天的版本

這次的完整改動集中在 user_entry.S、trap.S、user.c 與 user.S。
範例仍是單 hart、單一使用者程式,Day 12 的多行程排程器尚未與使用者位址空間整合。

建議 commit:

day17: enter user mode and dispatch system calls

下一篇把同樣的請求路徑接到磁碟,讓使用者程式拿到的文字不再藏在自己的程式映像中。

參考資料


上一篇
Day 16|User Process:第一次讓 Kernel 執行自己的 Application
下一篇
Day 18|從 VirtIO 到檔案:把 Tiny Kernel 變成一個小型 OS
系列文
打造 OS Kernel:從 OS in 1,000 Lines 到 xv6 共 22 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言